diff --git a/SITES.md b/SITES.md
index 57bd1c66..83bebeb6 100644
--- a/SITES.md
+++ b/SITES.md
@@ -130,6 +130,7 @@
| [sky.com](sites/sky.com) | 🟡 | https://github.com/iptv-org/epg/issues/2325 |
| [sky.de](sites/sky.de) | 🟢 | |
| [skylife.co.kr](sites/skylife.co.kr) | 🟢 | |
+| [sporttv.pt](sites/sporttv.pt) | 🟢 | |
| [starhubtvplus.com](sites/starhubtvplus.com) | 🔴 | https://github.com/iptv-org/epg/issues/2365 |
| [startimestv.com](sites/startimestv.com) | 🔴 | https://github.com/iptv-org/epg/issues/2296 |
| [streamingtvguides.com](sites/streamingtvguides.com) | 🟢 | |
diff --git a/sites/sporttv.pt/__data__/content.html b/sites/sporttv.pt/__data__/content.html
new file mode 100644
index 00000000..558cf5a2
--- /dev/null
+++ b/sites/sporttv.pt/__data__/content.html
@@ -0,0 +1,61675 @@
+
+
+
+
+
+ sport tv - Guia TV
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
00:00
+
01:00
+
02:00
+
03:00
+
04:00
+
05:00
+
06:00
+
07:00
+
08:00
+
09:00
+
10:00
+
11:00
+
12:00
+
13:00
+
14:00
+
15:00
+
16:00
+
17:00
+
18:00
+
19:00
+
20:00
+
21:00
+
22:00
+
23:00
+
00:00
+
01:00
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sites/sporttv.pt/readme.md b/sites/sporttv.pt/readme.md
new file mode 100644
index 00000000..06b32bb0
--- /dev/null
+++ b/sites/sporttv.pt/readme.md
@@ -0,0 +1,15 @@
+# sporttv.pt
+
+https://www.sporttv.pt/guia
+
+### Download the guide
+
+```sh
+npm run grab --- --site=sporttv.pt
+```
+
+### Test
+
+```sh
+npm test --- sporttv.pt
+```
diff --git a/sites/sporttv.pt/sporttv.pt.channels.xml b/sites/sporttv.pt/sporttv.pt.channels.xml
new file mode 100644
index 00000000..4cdac74b
--- /dev/null
+++ b/sites/sporttv.pt/sporttv.pt.channels.xml
@@ -0,0 +1,12 @@
+
+
+ SPORT.TV +
+ SPORT.TV1
+ SPORT.TV2
+ SPORT.TV3
+ SPORT.TV4
+ SPORT.TV5
+ SPORT.TV6
+ SPORT.TV7
+ NBA TV
+
diff --git a/sites/sporttv.pt/sporttv.pt.config.js b/sites/sporttv.pt/sporttv.pt.config.js
new file mode 100644
index 00000000..201b98ed
--- /dev/null
+++ b/sites/sporttv.pt/sporttv.pt.config.js
@@ -0,0 +1,63 @@
+const dayjs = require('dayjs')
+const cheerio = require('cheerio')
+
+module.exports = {
+ site: 'sporttv.pt',
+ days: 2,
+ url: 'https://www.sporttv.pt/guia',
+ parser({ content, date, channel }) {
+ let programs = []
+ const items = parseItems(content, channel, date)
+ items.forEach(item => {
+ const start = dayjs(item.data)
+ const stop = start.add(item.duracao, 'ms')
+
+ programs.push({
+ title: item.descricao,
+ description: item?.evento?.nome,
+ image: item.imagem,
+ category: item?.modalidade?.nomeModalidade,
+ start,
+ stop
+ })
+ })
+
+ return programs
+ }
+}
+
+function parseItems(content, channel, date) {
+ const $ = cheerio.load(content)
+ const nuxtData = $('#__NUXT_DATA__').html()
+ if (!nuxtData) return []
+ const parsed = JSON.parse(nuxtData)
+ const dataIndex = parsed[1].data
+ const epgIndex = Object.values(parsed[dataIndex])[3] // 1611
+ const epg = parsed[epgIndex].map(i => parsed[i]).map(obj => dataMapper(obj, parsed))
+ if (!Array.isArray(epg)) return []
+
+ return epg
+ .filter(
+ item => item.canal.id === parseInt(channel.site_id) && date.isSame(dayjs(item.data), 'd')
+ )
+ .sort((a, b) => {
+ if (a < b) return -1
+ if (a > b) return 1
+ return 0
+ })
+}
+
+function dataMapper(object, parsed) {
+ let output = {}
+
+ for (let key in object) {
+ const value = parsed[object[key]]
+ if (typeof value === 'object') {
+ output[key] = dataMapper(value, parsed)
+ } else {
+ output[key] = value
+ }
+ }
+
+ return output
+}
diff --git a/sites/sporttv.pt/sporttv.pt.test.js b/sites/sporttv.pt/sporttv.pt.test.js
new file mode 100644
index 00000000..a0836afe
--- /dev/null
+++ b/sites/sporttv.pt/sporttv.pt.test.js
@@ -0,0 +1,54 @@
+const { parser, url } = require('./sporttv.pt.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 date = dayjs.utc('2024-12-23', 'YYYY-MM-DD').startOf('d')
+const channel = {
+ site_id: 727,
+ xmltv_id: 'SportTV1.pt'
+}
+
+it('can generate valid url', () => {
+ expect(url).toBe('https://www.sporttv.pt/guia')
+})
+
+it('can parse response', () => {
+ const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'))
+ const results = parser({ content, date, channel }).map(p => {
+ p.start = p.start.toJSON()
+ p.stop = p.stop.toJSON()
+ return p
+ })
+
+ expect(results.length).toBe(19)
+
+ expect(results[0]).toMatchObject({
+ start: '2024-12-23T01:00:00.000Z',
+ stop: '2024-12-23T01:30:00.000Z',
+ description: 'LIGA PORTUGAL BETCLIC',
+ category: 'FUTEBOL',
+ title: 'RESUMOS DA JORNADA 15',
+ image: 'https://www.sporttv.pt/default/0001/11/08cb25f0b9b427e0bb83179309074632410f536b.jpg'
+ })
+
+ expect(results[1]).toMatchObject({
+ start: '2024-12-23T01:30:00.000Z',
+ stop: '2024-12-23T02:00:00.000Z',
+ description: 'LIGA ITALIANA',
+ category: 'FUTEBOL',
+ title: 'RESUMOS DA JORNADA 17',
+ image:
+ 'https://www.sporttv.pt/cms_media/default/0001/11/56ab6bb72a00c8a9543eff35f90f57c07fb0ff87.jpg'
+ })
+})
+
+it('can handle empty guide', () => {
+ const content = ''
+ const result = parser({ content, date })
+ expect(result).toMatchObject([])
+})