updating dependencies & fixed tv.dir.bg

This commit is contained in:
theofficialomega
2025-07-27 16:14:59 +02:00
parent 2a658185d3
commit d6884090df
8 changed files with 2268 additions and 644 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,4 @@
{
"status": true,
"html": "<div class=\"panels\">\n <div class=\"panel\">\n <div class=\"day-broadcast-list\">\n <p class=\"broadcast-item-name\">\n 29.07\n </p>\n <div class=\"broadcasts-lists\">\n </div>\n </div>\n </div>\n <div class=\"panel\">\n <div class=\"day-broadcast-list\">\n <p class=\"broadcast-item-name\">\n 30.07\n </p>\n <div class=\"broadcasts-lists\">\n </div>\n </div>\n </div>\n <div class=\"panel\">\n <div class=\"day-broadcast-list\">\n <p class=\"broadcast-item-name\">\n 31.07\n </p>\n <div class=\"broadcasts-lists\">\n </div>\n </div>\n </div>\n </div>"
}

View File

@@ -1 +0,0 @@
<div class=\"panels\">\n <div class=\"panel\">\n <div class=\"day-broadcast-list\">\n <p class=\"broadcast-item-name\">\n 19.02\n </p>\n <div class=\"broadcasts-lists\">\n </div>\n </div>\n </div>\n <div class=\"panel\">\n <div class=\"day-broadcast-list\">\n <p class=\"broadcast-item-name\">\n 20.02\n </p>\n <div class=\"broadcasts-lists\">\n </div>\n </div>\n </div>\n <div class=\"panel\">\n <div class=\"day-broadcast-list\">\n <p class=\"broadcast-item-name\">\n 21.02\n </p>\n <div class=\"broadcasts-lists\">\n </div>\n </div>\n </div>\n </div>

View File

@@ -1,52 +1,134 @@
const axios = require('axios')
const cheerio = require('cheerio')
const url = require('url')
const { DateTime } = require('luxon')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat')
let cachedToken = null
let tokenExpiry = null
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
let sessionCache = null
async function getSession(forceRefresh = false) {
if (sessionCache && !forceRefresh) {
return sessionCache
}
try {
const initResponse = await axios.get('https://tv.dir.bg/init')
if (!initResponse.data) {
throw new Error('No response data from init endpoint')
}
// Extract cookies from response headers
const setCookieHeader = initResponse.headers['set-cookie']
let xsrfToken = null
let dirSessionCookie = null
if (setCookieHeader) {
setCookieHeader.forEach(cookie => {
// Extract XSRF token from cookie
const xsrfMatch = cookie.match(/XSRF-TOKEN=([^;]+)/)
if (xsrfMatch) {
xsrfToken = decodeURIComponent(xsrfMatch[1])
}
// Extract dir_session cookie
const sessionMatch = cookie.match(/dir_session=([^;]+)/)
if (sessionMatch) {
dirSessionCookie = sessionMatch[1]
}
})
}
const csrfToken = initResponse.data.csrfToken
if (!csrfToken) {
throw new Error('No CSRF/XSRF token found in response')
}
// Build cookie string
let cookieString = ''
if (xsrfToken) {
cookieString += `XSRF-TOKEN=${encodeURIComponent(xsrfToken)}`
}
if (dirSessionCookie) {
if (cookieString) cookieString += '; '
cookieString += `dir_session=${dirSessionCookie}`
}
sessionCache = {
csrfToken,
cookieString,
timestamp: Date.now()
}
return sessionCache
} catch (error) {
console.error('Error getting session:', error.message)
throw error
}
}
module.exports = {
site: 'tv.dir.bg',
days: 2,
async url({ channel, date }) {
const token = await getToken()
if (!token) {
throw new Error('Unable to retrieve CSRF token')
}
const form = new url.URLSearchParams({
_token: token,
channel: channel.site_id,
day: date.format('YYYY-MM-DD')
})
return axios.post('https://tv.dir.bg/load/programs', form.toString(), {
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'X-Requested-With': 'XMLHttpRequest'
url: 'https://tv.dir.bg/load/programs',
request: {
maxContentLength: 125000000, // 10 MB
method: 'POST',
async headers() {
try {
const session = await getSession()
return {
'Cookie': session.cookieString,
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'X-Requested-With': 'XMLHttpRequest'
}
} catch (error) {
console.error('Error getting headers:', error.message)
throw error
}
})
},
async data({ channel, date }) {
try {
const session = await getSession()
const params = new URLSearchParams()
params.append('_token', session.csrfToken)
params.append('channel', channel.site_id)
params.append('day', date.format('YYYY-MM-DD'))
return params
} catch (error) {
console.error('Error preparing request data:', error.message)
throw error
}
},
},
parser({ content, date }) {
const programs = []
const items = parseItems(content)
items.forEach(item => {
const $item = cheerio.load(item)
const prev = programs[programs.length - 1]
const $item = cheerio.load(item)
let start = parseStart($item, date)
if (!start) return
if (prev) {
if (start < prev.start) {
start = start.plus({ days: 1 })
date = date.plus({ days: 1 })
if (start.isBefore(prev.start)) {
start = start.add(1, 'd')
}
prev.stop = start
}
const stop = start.plus({ minutes: 30 })
const stop = start.add(30, 'm')
programs.push({
title: parseTitle($item),
start,
@@ -56,6 +138,7 @@ module.exports = {
return programs
},
async channels() {
try {
const response = await axios.get('https://tv.dir.bg/channels')
@@ -63,7 +146,7 @@ module.exports = {
const channels = []
$('.channel_cont').each((index, element) => {
$('.channel_cont').each((_index, element) => {
const $element = $(element)
const $link = $element.find('a.channel_link')
@@ -79,63 +162,55 @@ module.exports = {
channels.push({
lang: 'bg',
site_id: site_id,
name: name,
logo: logo
name: name.trim(),
logo: logo ? (logo.startsWith('http') ? logo : `https://tv.dir.bg${logo}`) : null
})
}
})
return channels
return channels
} catch (error) {
console.error('Error fetching channels:', error)
return []
}
}
}
async function getToken() {
if (cachedToken && tokenExpiry && DateTime.now() < tokenExpiry) {
return cachedToken
}
try {
const response = await axios.get('https://tv.dir.bg/init', { headers: {'X-Requested-With': 'XMLHttpRequest'} })
// Check different possible locations for the token
let token = null
if (response.data && response.data.csrfToken) {
token = response.data.csrfToken
} catch (error) {
console.error('Error fetching channels:', error.message)
return []
}
if (token) {
cachedToken = token
tokenExpiry = DateTime.now().plus({ hours: 1 })
return token
} else {
console.error('CSRF token not found in response structure:', Object.keys(response.data || {}))
return null
}
} catch (error) {
console.error('Error fetching token:', error.message)
return null
},
clearSession() {
sessionCache = null
}
}
function parseStart($item, date) {
const timeText = $item('.broadcast-time').text().trim()
if (!timeText) return null
const time = $item('.broadcast-time').text().trim()
const dateString = `${date.format('YYYY-MM-DD')} ${time}`
const [hours, minutes] = timeText.split(':').map(Number)
const dateTime = date.isValid ? date : DateTime.fromISO(date)
return dateTime.set({ hour: hours, minute: minutes, second: 0, millisecond: 0 })
return dayjs.tz(dateString, 'YYYY-MM-DD HH:mm', 'Europe/Sofia')
}
function parseTitle($item) {
return $item('.broadcast-title').text().trim()
return $item('.broadcast-title').text()
.replace(/\s+/g, ' ')
.trim()
}
function parseItems(content) {
const $ = cheerio.load(content)
return $('.broadcast-item').toArray()
}
try {
const json = JSON.parse(content)
if (!json || json.status !== true) {
return []
}
const $ = cheerio.load(json.html)
const items = $('.broadcast-item').toArray()
return items
} catch (error) {
console.error('❌ Error parsing items:', error.message)
console.error('Error stack:', error.stack)
return []
}
}

View File

@@ -9,46 +9,42 @@ dayjs.extend(utc)
const date = dayjs.utc('2025-06-30', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '12',
site_id: '61',
xmltv_id: 'BTV.bg'
}
it('can generate valid url', () => {
expect(url({ channel, date })).toBe('https://tv.dir.bg/programa/12')
expect(url).toBe('https://tv.dir.bg/load/programs')
})
it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'))
const result = parser({ content, date }).map(p => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const results = parser({ content, date }).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(result).toMatchObject([
{
start: '2025-06-30T08:00:00.000Z',
stop: '2025-06-30T10:00:00.000Z',
title: 'Купа на Франция: Еспали - Пари Сен Жермен'
},
{
start: '2025-06-30T10:00:00.000Z',
stop: '2025-06-30T12:00:00.000Z',
title: 'Ла Лига: Леганес - Реал Сосиедад'
},
{
start: '2025-06-30T12:00:00.000Z',
stop: '2025-06-30T13:00:00.000Z',
title: 'Пред Стадиона&quot; - спортно шоу'
}
])
expect(results.length).toBe(63)
expect(results[0]).toMatchObject({
start: '2025-06-30T03:00:00.000Z',
stop: '2025-06-30T03:30:00.000Z',
title: 'Светът на здравето'
})
expect(results[62]).toMatchObject({
start: '2025-07-01T02:00:00.000Z',
stop: '2025-07-01T02:30:00.000Z',
title: 'Убийства в Рая , сезон 1 , епизод 7'
})
})
it('can handle empty guide', () => {
const result = parser({
date,
channel,
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_data.html'))
content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.json'))
})
expect(result).toMatchObject([])
})