diff --git a/.github/workflows/tv24.se.yml b/.github/workflows/tv24.se.yml
new file mode 100644
index 00000000..77af928f
--- /dev/null
+++ b/.github/workflows/tv24.se.yml
@@ -0,0 +1,17 @@
+name: tv24.se
+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/tv24.se/__data__/content.html b/sites/tv24.se/__data__/content.html
new file mode 100644
index 00000000..554f3beb
--- /dev/null
+++ b/sites/tv24.se/__data__/content.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/sites/tv24.se/tv24.se.config.js b/sites/tv24.se/tv24.se.config.js
new file mode 100644
index 00000000..32a5ed7d
--- /dev/null
+++ b/sites/tv24.se/tv24.se.config.js
@@ -0,0 +1,92 @@
+const dayjs = require('dayjs')
+const axios = require('axios')
+const cheerio = require('cheerio')
+const utc = require('dayjs/plugin/utc')
+const customParseFormat = require('dayjs/plugin/customParseFormat')
+
+dayjs.extend(utc)
+dayjs.extend(customParseFormat)
+
+module.exports = {
+ site: 'tv24.se',
+ url: function ({ channel, date }) {
+ return `https://tv24.se/x/channel/${channel.site_id}/0/${date.format('YYYY-MM-DD')}`
+ },
+ parser: function ({ content, date }) {
+ let programs = []
+ const items = parseItems(content)
+ items.forEach(item => {
+ const prev = programs[programs.length - 1]
+ const $item = cheerio.load(item)
+ let start = parseStart($item, date)
+ 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),
+ description: parseDescription($item),
+ start,
+ stop
+ })
+ })
+
+ return programs
+ },
+ async channels() {
+ let html = await axios
+ .get(`https://tv24.se/x/settings/addremove`)
+ .then(r => r.data)
+ .catch(console.log)
+ let $ = cheerio.load(html)
+ const nums = $('li')
+ .toArray()
+ .map(item => $(item).data('channel'))
+ html = await axios
+ .get(`https://tv24.se`, {
+ headers: {
+ Cookie: `selectedChannels=${nums.join(',')}`
+ }
+ })
+ .then(r => r.data)
+ .catch(console.log)
+ $ = cheerio.load(html)
+ const items = $('li.c').toArray()
+
+ return items.map(item => {
+ const name = $(item).find('h3').text().trim()
+ const link = $(item).find('.channel').attr('href')
+ const [_, site_id] = link.match(/\/kanal\/(.*)/) || [null, null]
+
+ return {
+ lang: 'sv',
+ site_id,
+ name
+ }
+ })
+ }
+}
+
+function parseTitle($item) {
+ return $item('h3').text()
+}
+
+function parseDescription($item) {
+ return $item('p').text()
+}
+
+function parseStart($item, date) {
+ const time = $item('.time')
+
+ return dayjs.utc(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm')
+}
+
+function parseItems(content) {
+ const $ = cheerio.load(content)
+
+ return $('.program').toArray()
+}
diff --git a/sites/tv24.se/tv24.se.test.js b/sites/tv24.se/tv24.se.test.js
new file mode 100644
index 00000000..e5d587b8
--- /dev/null
+++ b/sites/tv24.se/tv24.se.test.js
@@ -0,0 +1,53 @@
+// npm run channels:parse -- --config=./sites/tv24.se/tv24.se.config.js --output=./sites/tv24.se/tv24.se_se.channels.xml
+// npx epg-grabber --config=sites/tv24.se/tv24.se.config.js --channels=sites/tv24.se/tv24.se_se.channels.xml --output=guide.xml --days=2
+
+const { parser, url } = require('./tv24.se.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('2022-08-26', 'YYYY-MM-DD').startOf('d')
+const channel = {
+ site_id: 'svt1',
+ xmltv_id: 'SVT1.se'
+}
+
+it('can generate valid url', () => {
+ expect(url({ channel, date })).toBe('https://tv24.se/x/channel/svt1/0/2022-08-26')
+})
+
+it('can parse response', () => {
+ const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'))
+ const results = parser({ content, date }).map(p => {
+ p.start = p.start.toJSON()
+ p.stop = p.stop.toJSON()
+ return p
+ })
+
+ expect(results[0]).toMatchObject({
+ start: '2022-08-26T04:00:00.000Z',
+ stop: '2022-08-26T07:10:00.000Z',
+ title: 'Morgonstudion',
+ description:
+ 'Dagens viktigaste nyheter och analyser med ständiga uppdateringar. Vi sänder direkt inrikes- och utrikesnyheter inklusive sport, kultur och nöje. Dessutom intervjuer med aktuella gäster. Nyhetssammanfattningar varje kvart med start kl 06.00.'
+ })
+
+ expect(results[33]).toMatchObject({
+ start: '2022-08-27T05:20:00.000Z',
+ stop: '2022-08-27T05:50:00.000Z',
+ title: 'Uppdrag granskning',
+ description:
+ 'När samtliga sex män frias för ännu en skjutning växer vreden inom polisen. Ökningen av skjutningar i Sverige ligger i topp i Europa - och nu är våldsspiralen på väg mot ett nattsvart rekord. Hur blev Sverige landet där mördare går fria?'
+ })
+})
+
+it('can handle empty guide', () => {
+ const result = parser({
+ content: ''
+ })
+ expect(result).toMatchObject([])
+})
diff --git a/sites/tv24.se/tv24.se_se.channels.xml b/sites/tv24.se/tv24.se_se.channels.xml
new file mode 100644
index 00000000..27d5ee52
--- /dev/null
+++ b/sites/tv24.se/tv24.se_se.channels.xml
@@ -0,0 +1,309 @@
+
+
+
+ 3sat
+ Al Jazeera (Arabic)
+ Al Jazeera (English)
+ Animal Planet HD
+ Animal Planet
+ ATG Live
+ BBC Brit
+ BBC Earth
+ BBC World News
+ Cartoon Network
+ CBS Reality
+ CGTN
+ C More First
+ C More First HD
+ C More Fotboll
+ C More Fotboll HD
+ C More Hits
+ C More Hits HD
+ C More Hockey
+ C More Hockey HD
+ C More Live
+ C More Live 2
+ C More Live 2 HD
+ C More Live 3
+ C More Live 3 HD
+ C More Live 4
+ C More Live 4 HD
+ C More Live 5
+ C More Live 5 HD
+ C More Live HD
+ C More Mix
+ C More Series
+ C More Series HD
+ C More Stars
+ C More Stars HD
+ CNBC
+ CNN International
+ Das Erste
+ Discovery Channel HD
+ Discovery Channel
+ Discovery+ Extra 1
+ Discovery+ Extra 2
+ Discovery+ Extra 3
+ Discovery+ Extra 4
+ Discovery+ Extra 5
+ Discovery+ Extra 6
+ Discovery+ Extra 7
+ Discovery+ Extra 8
+ Disney Channel
+ Disney Junior
+ DR1
+ DR2
+ DR Ramasjang
+ Duna TV
+ Eurosport 1 HD
+ Eurosport 1
+ Eurosport 2 HD
+ Eurosport 2
+ Extreme Sports Channel
+ Fight Sports
+ Food Network
+ Fuel TV
+ Godare
+ Gospel Channel
+ Hayat Plus
+ History
+ Horse & Country (UK)
+ Horse & Country TV
+ HRT1
+ Kanal 10
+ Kanal 11
+ Kanal 4
+ Kanal 5
+ Kanal 5 HD
+ Kanal 9
+ Kunskapskanalen
+ Las Estrellas
+ Mezzo
+ Mezzo Live HD
+ Motorvision TV
+ MTV3
+ MTV 80s
+ MTV 90s
+ National Geographic HD
+ National Geographic
+ National Geographic Wild HD
+ National Geographic Wild
+ Nautical Channel
+ Nickelodeon
+ Nick JR
+ Nicktoons
+ NRK3
+ NRK Super
+ OUTtv
+ Pink Plus
+ ProSieben
+ Pro TV int
+ Rai 1
+ Rai 2
+ Rai 3
+ RTP Internacional Europe
+ SF Kanalen
+ Sjuan
+ Sky Cinema Premiere +1
+ Sky History 2 HD
+ Sky News
+ Sportkanalen
+ SVT1
+ SVT1 HD
+ SVT2
+ SVT24
+ SVT24 HD
+ SVT2 HD
+ SVT Barn
+ TLC
+ Travel Channel
+ TV10
+ TV12
+ TV2 Charlie
+ TV2 News
+ TV2 Nyhetskanalen
+ TV 2 Zebra
+ TV2 Zulu
+ TV3 Plus
+ TV3
+ TV4
+ TV4 Fakta
+ TV4 Film
+ TV4 Guld
+ TV4 HD
+ TV5 Monde Europe
+ TV6
+ TV8
+ TVCG Sat
+ TV Chile
+ TVE Internacional
+ TVNorge
+ TVP Polonia
+ V4
+ V Film Action HD
+ V Film Family
+ V Film Hits HD
+ V Film Premiere HD
+ Viasat Explore
+ Viasat Explore HD
+ Viasat History
+ Viasat History HD
+ Viasat Nature
+ Viasat Nature HD
+ Viasat Sport
+ V Sport Extra
+ V Sport Extra HD
+ V Sport Fotboll HD
+ V Sport Motor HD
+ V Sport Premium HD
+ V Sport Ultra HD
+ V Sport Vinter
+ Yle Teema & Fem HD
+ Yle TV1
+ Yle TV1 HD
+ Yle TV2
+ Yle TV2 HD
+ ZDF
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+