epg/sites/tv.yandex.ru/tv.yandex.ru.config.js
Toha a5229d3af0 Simultaneous fetch is now external package.
Signed-off-by: Toha <tohenk@yahoo.com>
2024-12-20 19:54:59 +07:00

282 lines
No EOL
8.5 KiB
JavaScript

const dayjs = require('dayjs')
const doFetch = require('@ntlab/sfetch')
const debug = require('debug')('site:tv.yandex.ru')
doFetch
.setDebugger(debug)
.setMaxWorker(10)
// enable to fetch guide description but its take a longer time
const detailedGuide = true
// update this data by heading to https://tv.yandex.ru and change the values accordingly
const cookies = {
i: 'eIUfSP+/mzQWXcH+Cuz8o1vY+D2K8fhBd6Sj0xvbPZeO4l3cY+BvMp8fFIuM17l6UE1Z5+R2a18lP00ex9iYVJ+VT+c=',
spravka: 'dD0xNzM0MjA0NjM4O2k9MTI1LjE2NC4xNDkuMjAwO0Q9QTVCQ0IyOTI5RDQxNkU5NkEyOTcwMTNDMzZGMDAzNjRDNTFFNDM4QkE2Q0IyOTJDRjhCOTZDRDIzODdBQzk2MzRFRDc5QTk2Qjc2OEI1MUY5MTM5M0QzNkY3OEQ2OUY3OTUwNkQ3RjBCOEJGOEJDMjAwMTQ0RDUwRkFCMDNEQzJFMDI2OEI5OTk5OUJBNEFERUYwOEQ1MjUwQTE0QTI3RDU1MEQwM0U0O3U9MTczNDIwNDYzODUyNDYyNzg1NDtoPTIxNTc0ZTc2MDQ1ZjcwMDBkYmY0NTVkM2Q2ZWMyM2Y1',
yandexuid: '1197179041732383499',
yashr: '4682342911732383504',
yuidss: '1197179041732383499',
user_display: 824,
}
const headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 OPR/114.0.0.0',
}
const caches = {}
module.exports = {
site: 'tv.yandex.ru',
days: 2,
url({ date }) {
return getUrl(date)
},
request: {
cache: {
ttl: 3600000 // 1 hour
},
headers: getHeaders()
},
async parser({ content, date, channel }) {
const programs = []
const events = []
if (content && parseContent(content, date, true)) {
const cacheid = date.format('YYYY-MM-DD')
if (!caches[cacheid]) {
debug(`Please wait while fetching schedules for ${cacheid}`)
caches[cacheid] = await fetchSchedules({ date, content })
}
if (detailedGuide) {
await fetchPrograms({ schedules: caches[cacheid], date, channel })
}
caches[cacheid].forEach(schedule => {
schedule.events
.filter(event => event.channelFamilyId == channel.site_id && date.isSame(event.start, 'day'))
.forEach(event => {
if (events.indexOf(event.id) < 0) {
events.push(event.id)
programs.push({
title: event.title,
description: event.program.description,
category: event.program.type.name,
start: dayjs(event.start),
stop: dayjs(event.finish)
})
}
})
})
}
return programs
},
async channels() {
const channels = []
const included = []
const schedules = await fetchSchedules({ date: dayjs() })
schedules.forEach(schedule => {
if (schedule.channel && included.indexOf(schedule.channel.familyId) < 0) {
included.push(schedule.channel.familyId)
channels.push({
lang: 'ru',
site_id: schedule.channel.familyId.toString(),
name: schedule.channel.title
})
}
})
return channels
}
}
async function fetchSchedules({ date, content = null }) {
const schedules = []
const queues = []
const fetches = []
const url = getUrl(date)
let mainApi
// parse content as schedules and add to queue if more requests is needed
const f = (src, res, headers) => {
if (src) {
fetches.push(src)
}
if (headers) {
parseCookies(headers)
}
const [q, s] = parseContent(res, date)
if (!mainApi) {
mainApi = true
if (caches.region) {
queues.push(getQueue(getUrl(date, caches.region), src))
}
}
for (const url of q) {
if (fetches.indexOf(url) < 0) {
queues.push(getQueue(url, src))
}
}
schedules.push(...s)
}
// is main html already fetched?
if (content) {
f(url, content)
} else {
queues.push(getQueue(url, 'https://tv.yandex.ru/'))
}
// fetch all queues
await doFetch(queues, f)
return schedules
}
async function fetchPrograms({ schedules, date, channel }) {
const queues = []
schedules
.filter(schedule => schedule.channel.familyId == channel.site_id)
.forEach(schedule => {
queues.push(
...schedule.events
.filter(event => date.isSame(event.start, 'day'))
.map(event => getQueue(getUrl(null, caches.region, null, event), 'https://tv.yandex.ru/'))
)
})
await doFetch(queues, (queue, res, headers) => {
if (headers) {
parseCookies(headers)
}
// is it a program?
if (res?.program) {
let updated = false
schedules.forEach(schedule => {
schedule.events.forEach(event => {
if (event.channelFamilyId === res.channelFamilyId && event.id === res.id) {
Object.assign(event, res)
updated = true
return true
}
})
if (updated) {
return true
}
})
}
})
}
function parseContent(content, date, checkOnly = false) {
const queues = []
const schedules = []
let valid = false
if (content) {
if (Buffer.isBuffer(content)) {
content = content.toString()
}
// got captcha, its look like our cookies has expired
if (content?.type === 'captcha' || (typeof content === 'string' && content.match(/SmartCaptcha/))) {
throw new Error('Got captcha, please goto https://tv.yandex.ru and update cookies!')
}
if (typeof content === 'object') {
let items
if (content.schedule) {
// fetch next request based on schedule map
if (Array.isArray(content.schedule.scheduleMap)) {
queues.push(...content.schedule.scheduleMap.map(m => getUrl(date, caches.region, m)))
}
// find some schedules?
if (Array.isArray(content.schedule.schedules)) {
items = content.schedule.schedules
}
}
// find another schedules?
if (Array.isArray(content.schedules)) {
items = content.schedules
}
// add programs
if (items && items.length) {
schedules.push(...getSchedules(items))
}
} else {
// prepare headers for next http request
const [, region] = content.match(/region: '(\d+)'/i) || [null, null]
const [, initialSk] = content.match(/window.__INITIAL_SK__ = (.*);/i) || [null, null]
const [, sessionId] = content.match(/window.__USER_SESSION_ID__ = "(.*)";/i) || [null, null]
const tvSk = initialSk ? JSON.parse(initialSk) : {}
if (region) {
caches.region = region
}
if (tvSk.key) {
headers['X-Tv-Sk'] = tvSk.key
}
if (sessionId) {
headers['X-User-Session-Id'] = sessionId
}
if (checkOnly && region && tvSk.key && sessionId) {
valid = true;
}
}
}
return checkOnly ? valid : [queues, schedules]
}
function parseCookies(headers) {
if (Array.isArray(headers['set-cookie'])) {
headers['set-cookie']
.forEach(cookie => {
const [key, value] = cookie.split('; ')[0].split('=')
if (cookies[key] !== value) {
cookies[key] = value
debug(`Update cookie ${key}=${value}`)
}
})
}
}
function getSchedules(schedules) {
return schedules.filter(schedule => schedule.events.length);
}
function getHeaders(data = {}) {
return Object.assign({}, headers, {
Cookie: Object.keys(cookies).map(cookie => `${cookie}=${cookies[cookie]}`).join('; ')
}, data)
}
function getUrl(date, region = null, page = null, event = null) {
let url = 'https://tv.yandex.ru/'
if (region) {
url += `api/${region}`
}
if (page && page.id !== undefined) {
url += `${url.endsWith('/') ? '' : '/'}main/chunk?page=${page.id}`
}
if (event && event.id !== undefined) {
url += `${url.endsWith('/') ? '' : '/'}event?eventId=${event.id}&programCoId=`
}
if (date) {
url += `${url.indexOf('?') < 0 ? '?' : '&'}date=${date.format('YYYY-MM-DD')}${!page ? '&grid=all' : ''}&period=all-day`
}
if (page && page.id !== undefined && page.offset !== undefined) {
url += `${url.indexOf('?') < 0 ? '?' : '&'}offset=${page.offset}`
}
if (page && page.id !== undefined && page.limit !== undefined) {
url += `${url.indexOf('?') < 0 ? '?' : '&'}limit=${page.limit}`
}
return url
}
function getQueue(url, referer) {
const data = {
'Origin': 'https://tv.yandex.ru',
}
if (referer) {
data['Referer'] = referer
}
if (url.indexOf('api') > 0) {
data['X-Requested-With'] = 'XMLHttpRequest'
}
const headers = getHeaders(data)
return {
url,
params: { headers }
}
}