diff --git a/.github/workflows/tivu.tv.yml b/.github/workflows/tivu.tv.yml
new file mode 100644
index 00000000..3a333620
--- /dev/null
+++ b/.github/workflows/tivu.tv.yml
@@ -0,0 +1,17 @@
+name: tivu.tv
+on:
+ schedule:
+ - cron: '0 3 * * *'
+ workflow_dispatch:
+ workflow_run:
+ workflows: [_trigger]
+ types:
+ - completed
+jobs:
+ load:
+ uses: ./.github/workflows/_load.yml
+ with:
+ site: ${{github.workflow}}
+ secrets:
+ APP_ID: ${{ secrets.APP_ID }}
+ APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }}
diff --git a/sites/tivu.tv/__data__/content.html b/sites/tivu.tv/__data__/content.html
new file mode 100644
index 00000000..1c5be10e
--- /dev/null
+++ b/sites/tivu.tv/__data__/content.html
@@ -0,0 +1,476 @@
+
diff --git a/sites/tivu.tv/__data__/no_content.html b/sites/tivu.tv/__data__/no_content.html
new file mode 100644
index 00000000..e69de29b
diff --git a/sites/tivu.tv/tivu.tv.config.js b/sites/tivu.tv/tivu.tv.config.js
new file mode 100644
index 00000000..1a758708
--- /dev/null
+++ b/sites/tivu.tv/tivu.tv.config.js
@@ -0,0 +1,68 @@
+const dayjs = require('dayjs')
+const cheerio = require('cheerio')
+const utc = require('dayjs/plugin/utc')
+const timezone = require('dayjs/plugin/timezone')
+const customParseFormat = require('dayjs/plugin/customParseFormat')
+
+dayjs.extend(utc)
+dayjs.extend(timezone)
+dayjs.extend(customParseFormat)
+
+module.exports = {
+ site: 'tivu.tv',
+ request: {
+ cache: {
+ ttl: 60 * 60 * 1000 // 1 hour
+ }
+ },
+ url({ date }) {
+ const diff = date.diff(dayjs.utc().startOf('d'), 'd')
+
+ return `https://www.tivu.tv/epg_ajax_sat.aspx?d=${diff}`
+ },
+ parser: function ({ content, channel, date }) {
+ let programs = []
+ const items = parseItems(content, channel, date)
+ items.forEach(item => {
+ const $item = cheerio.load(item)
+ const prev = programs[programs.length - 1]
+ let start = parseStart($item, date)
+ if (!start) return
+ if (prev) {
+ if (start.isBefore(prev.start)) {
+ start = start.add(1, 'd')
+ date = date.add(1, 'd')
+ }
+ prev.stop = start
+ }
+ const stop = start.add(30, 'm')
+ programs.push({
+ title: parseTitle($item),
+ start,
+ stop
+ })
+ })
+
+ return programs
+ }
+}
+
+function parseTitle($item) {
+ const [title, _, __] = $item('a').html().split('
')
+
+ return title
+}
+
+function parseStart($item, date) {
+ const [_, __, time] = $item('a').html().split('
')
+ if (!time) return null
+
+ return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Europe/Rome')
+}
+
+function parseItems(content, channel, date) {
+ if (!content) return []
+ const $ = cheerio.load(content)
+
+ return $(`.q[id="${channel.site_id}"] > .p`).toArray()
+}
diff --git a/sites/tivu.tv/tivu.tv.test.js b/sites/tivu.tv/tivu.tv.test.js
new file mode 100644
index 00000000..4f53df5f
--- /dev/null
+++ b/sites/tivu.tv/tivu.tv.test.js
@@ -0,0 +1,54 @@
+// npx epg-grabber --config=sites/tivu.tv/tivu.tv.config.js --channels=sites/tivu.tv/tivu.tv_it.channels.xml --output=guide.xml --days=2
+
+const { parser, url, request } = require('./tivu.tv.config.js')
+const fs = require('fs')
+const path = require('path')
+const dayjs = require('dayjs')
+const utc = require('dayjs/plugin/utc')
+const customParseFormat = require('dayjs/plugin/customParseFormat')
+dayjs.extend(customParseFormat)
+dayjs.extend(utc)
+const axios = require('axios')
+jest.mock('axios')
+
+const date = dayjs.utc('2022-10-04', 'YYYY-MM-DD').startOf('d')
+const channel = {
+ site_id: '62',
+ xmltv_id: 'Rai1HD.it'
+}
+
+it('can generate valid url for today', () => {
+ expect(url({ date })).toBe('https://www.tivu.tv/epg_ajax_sat.aspx?d=0')
+})
+
+it('can generate valid url for tomorrow', () => {
+ expect(url({ date: date.add(1, 'd') })).toBe('https://www.tivu.tv/epg_ajax_sat.aspx?d=1')
+})
+
+it('can parse response', () => {
+ const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'))
+
+ let results = parser({ content, channel, date }).map(p => {
+ p.start = p.start.toJSON()
+ p.stop = p.stop.toJSON()
+ return p
+ })
+
+ expect(results[0]).toMatchObject({
+ start: '2022-10-03T22:02:00.000Z',
+ stop: '2022-10-03T22:45:00.000Z',
+ title: 'Cose Nostre - La figlia del boss'
+ })
+
+ expect(results[43]).toMatchObject({
+ start: '2022-10-05T04:58:00.000Z',
+ stop: '2022-10-05T05:28:00.000Z',
+ title: 'Tgunomattina - in collaborazione con day'
+ })
+})
+
+it('can handle empty guide', () => {
+ const content = fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'))
+ const result = parser({ content, channel, date })
+ expect(result).toMatchObject([])
+})
diff --git a/sites/tivu.tv/tivu.tv_it.channels.xml b/sites/tivu.tv/tivu.tv_it.channels.xml
new file mode 100644
index 00000000..9eb8c647
--- /dev/null
+++ b/sites/tivu.tv/tivu.tv_it.channels.xml
@@ -0,0 +1,75 @@
+
+
+
+ 20 Mediaset
+ 27
+ Al Jazeera
+ BBC World News
+ Bloomberg European TV
+ Boing
+ Canale 5
+ Cartoonito
+ cielo
+ Cine34
+ DMAX
+ UNIQtv HD
+ Euronews Italian
+ Fashion TV
+ Focus
+ Food Network
+ France 24 HD (in English)
+ France 24 HD (en Français)
+ frisbee
+ GIALLO
+ Gold TV
+ HGTV
+ Horse TV
+ Iris
+ Italia 1
+ Mediaset ITALIA DUE
+ K2
+ KBS HD
+ La 5
+ LA7
+ LA7d
+ Mediaset EXTRA
+ MEZZO
+ Motor Trend
+ Museum
+ MyZen TV
+ NASA
+ NHK WORLD-JAPAN
+ NOVE
+ QVC
+ Radio Italia TV
+ RMC
+ Rai 1
+ Rai 2
+ Rai 3
+ Rai 4
+ Rai 4K
+ Rai 5
+ Rai Gulp
+ Rai Movie
+ Rai News 24
+ Rai Premium
+ Rai Scuola
+ Rai Sport
+ Rai Storia
+ Rai yoyo
+ RDS Social TV
+ Real Time
+ Rete 4
+ RTL 102.5
+ Super!
+ TgCom24
+ Topcrime
+ Travel XP
+ TRM h24
+ TRT World HD
+ TV2000
+ TV8
+ VH1
+
+
+