diff --git a/.github/workflows/virginmediatelevision.ie.yml b/.github/workflows/virginmediatelevision.ie.yml
new file mode 100644
index 00000000..44da1cb6
--- /dev/null
+++ b/.github/workflows/virginmediatelevision.ie.yml
@@ -0,0 +1,17 @@
+name: virginmediatelevision.ie
+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/virginmediatelevision.ie/__data__/content.html b/sites/virginmediatelevision.ie/__data__/content.html
new file mode 100644
index 00000000..f8539bc5
--- /dev/null
+++ b/sites/virginmediatelevision.ie/__data__/content.html
@@ -0,0 +1,591 @@
+
+
+
+
+
+

+

+
+
+

+

+
+
+

+

+
+
+

+

+
+
+

+

+
+
+
+
+
+
+
Chasing Shadows
+ 12.00am - 1.00am
+
+
+
+
Vicky Phelan: The Big Interview
+ 1.00am - 2.00am
+
+
+
+
Off Air
+ 2.00am - 5.25am
+
+
+
+
The Tonight Show
+ 5.25am - 6.15am [S]
+
+
+
+
The Six O'Clock Show
+ 6.15am - 7.00am [S]
+
+
+
+
Ireland AM
+ 7.00am - 10.00am [S]
+
+
+
+
This Morning
+ 10.00am - 12.30pm
+
+
+
+
News at 12.30
+ 12.30pm - 1.00pm [S]
+
+
+
+
The Chase
+ 1.00pm - 2.00pm [S]
+
+
+
+
The Chase
+ 2.00pm - 3.00pm [S]
+
+
+
+
Loose Women
+ 3.00pm - 4.00pm
+
+
+
+
Heathrow: Britain's Busiest Airport
+ 4.00pm - 4.30pm
+
+
+
+
Judge Judy
+ 4.30pm - 5.00pm
+
+
+
+
Judge Judy
+ 5.00pm - 5.30pm
+
+
+
+
News at 5.30
+ 5.30pm - 6.05pm [S]
+
+
+
+
The Six O'Clock Show
+ 6.05pm - 7.00pm [S]
+
+
+
+
News at 7
+ 7.00pm - 7.30pm [S]
+
+
+
+
Emmerdale
+ 7.30pm - 8.00pm [S]
+
+
+
+
Paul O'Grady: For the Love of Dogs
+ 8.00pm - 8.30pm [S]
+
+
+
+
Call the Cleaners
+ 8.30pm - 9.00pm [S]
+
+
+
+
Murder in the Family
+ 9.00pm - 10.00pm
+
+
+
+
The Tonight Show
+ 10.00pm - 11.00pm [S]
+
+
+
+
Who Wants to Be a Millionaire?
+ 11.00pm - 12.00am
+
+
+
+
+
+
The Jonathan Ross Show
+ 11.05pm - 12.05am
+
+
+
+
Love Island
+ 12.05am - 1.05am
+
+
+
+
Olivia Meets Her Match
+ 1.05am - 2.05am
+
+
+
+
Killing Spree
+ 2.05am - 2.55am
+
+
+
+
Off Air
+ 2.55am - 7.00am
+
+
+
+
Nanny 911
+ 7.00am - 8.00am
+
+
+
+
Tipping Point
+ 8.00am - 9.00am [S]
+
+
+
+
The Chase
+ 9.00am - 10.00am [S]
+
+
+
+
The Chase
+ 10.00am - 11.00am [S]
+
+
+
+
Hello Campers
+ 11.00am - 12.00pm
+
+
+
+
The National Lottery: 5-Star Family Reunion
+ 12.00pm - 1.00pm
+
+
+
+
Heathrow: Britain's Busiest Airport
+ 1.00pm - 2.00pm
+
+
+
+
The Deal Makers
+ 2.00pm - 3.00pm
+
+
+
+
The Deal Makers
+ 3.00pm - 4.00pm
+
+
+
+
Tipping Point
+ 4.00pm - 5.00pm [S]
+
+
+
+
Tipping Point
+ 5.00pm - 6.00pm [S]
+
+
+
+
The Chase
+ 6.00pm - 7.00pm [S]
+
+
+
+
The Chase
+ 7.00pm - 8.00pm [S]
+
+
+
+
The Family Chase
+ 8.00pm - 9.00pm
+
+
+
+
Love Island
+ 9.00pm - 10.05pm
+
+
+
+
10 Years Younger in 10 Days
+ 10.05pm - 11.10pm
+
+
+
+
The Jonathan Ross Show
+ 11.10pm - 12.15am
+
+
+
+
+
Coronation Street
+ 11.40pm - 12.10am [S]
+
+
+
+
Coronation Street
+ 12.10am - 12.40am [S]
+
+
+
+
Emmerdale
+ 12.40am - 1.10am [S]
+
+
+
+
Cleaning Up
+ 1.10am - 2.05am [S]
+
+
+
+
Off Air
+ 2.05am - 7.15am
+
+
+
+
The Chase Australia
+ 7.15am - 8.00am
+
+
+
+
Yes Chef
+ 8.00am - 9.00am [S]
+
+
+
+
Loose Women
+ 9.00am - 10.00am
+
+
+
+
Judge Judy
+ 10.00am - 10.30am
+
+
+
+
Judge Judy
+ 10.30am - 11.00am
+
+
+
+
The Deal Makers
+ 11.00am - 12.00pm
+
+
+
+
Storage Hoarders
+ 12.00pm - 1.00pm
+
+
+
+
The Good Witch
+ 1.00pm - 2.00pm
+
+
+
+
Emmerdale
+ 2.00pm - 2.30pm [S]
+
+
+
+
Coronation Street
+ 2.30pm - 3.00pm [S]
+
+
+
+
Coronation Street
+ 3.00pm - 3.30pm [S]
+
+
+
+
Keeping Canada Safe
+ 3.30pm - 4.00pm
+
+
+
+
Paul O'Grady: The Sally Army and Me
+ 4.00pm - 4.35pm [S]
+
+
+
+
Heartbeat
+ 4.35pm - 5.45pm [S]
+
+
+
+
Agatha Christie's Poirot
+ 5.45pm - 7.55pm [S]
+
+
+
+
Heartbeat
+ 7.55pm - 9.00pm [S]
+
+
+
+
Vera
+ 9.00pm - 11.00pm [S]
+
+
+
+
Emmerdale
+ 11.00pm - 11.30pm [S]
+
+
+
+
Killing Spree
+ 11.30pm - 12.30am
+
+
+
+
+
Formula E Highlights
+ 11.30pm - 12.30am
+
+
+
+
Paramedics
+ 12.30am - 1.30am [S]
+
+
+
+
Off Air
+ 1.30am - 7.00am
+
+
+
+
5 Gold Rings
+ 7.00am - 8.00am
+
+
+
+
5 Gold Rings
+ 8.00am - 9.00am
+
+
+
+
Vet School
+ 9.00am - 10.00am [S]
+
+
+
+
Vet School
+ 10.00am - 11.00am [S]
+
+
+
+
Tipping Point: Lucky Stars
+ 11.00am - 12.00pm [S]
+
+
+
+
Heathrow: Britain's Busiest Airport
+ 12.00pm - 12.30pm
+
+
+
+
Heathrow: Britain's Busiest Airport
+ 12.30pm - 1.00pm
+
+
+
+
Paramedics
+ 1.00pm - 2.00pm [S]
+
+
+
+
The National Lottery: 5-Star Family Reunion
+ 2.00pm - 3.00pm
+
+
+
+
Dog Rescue
+ 3.00pm - 3.30pm
+
+
+
+
Wild China with Ray Mears
+ 3.30pm - 4.05pm
+
+
+
+
The Motorway: Life in the Fast Lane
+ 4.05pm - 5.00pm
+
+
+
+
The National Lottery: 5-Star Family Reunion
+ 5.00pm - 6.00pm
+
+
+
+
Drive
+ 6.00pm - 7.00pm
+
+
+
+
The Deal Makers
+ 7.00pm - 8.00pm
+
+
+
+
Arthur & George
+ 8.00pm - 9.00pm
+
+
+
+
Judge Rinder's Crime Stories
+ 9.00pm - 10.00pm
+
+
+
+
Judge Rinder's Crime Stories
+ 10.00pm - 11.00pm
+
+
+
+
The Jonathan Ross Show
+ 11.00pm - 11.30pm
+
+
+
+
Sport Stories
+ 11.30pm - 12.30am [S]
+
+
+
+
+
Vera
+ 11.30pm - 1.10am [S]
+
+
+
+
Formula E Highlights
+ 1.10am - 2.05am
+
+
+
+
Off Air
+ 2.05am - 1.10pm
+
+
+
+
Sport Stories
+ 1.10pm - 2.00pm
+
+
+
+
Sport Stories
+ 2.00pm - 2.50pm
+
+
+
+
Sport Stories
+ 2.50pm - 3.40pm
+
+
+
+
Niall Quinn: Bootroom to Boardroom
+ 3.40pm - 4.30pm
+
+
+
+
Best of UEFA Champions League
+ 4.30pm - 5.25pm
+
+
+
+
Best of UEFA Champions League
+ 5.25pm - 6.20pm
+
+
+
+
Classic Six Nations
+ 6.20pm - 7.15pm
+
+
+
+
Classic Six Nations
+ 7.15pm - 8.10pm
+
+
+
+
Impossible Engineering
+ 8.10pm - 9.00pm
+
+
+
+
Our House
+ 9.00pm - 9.50pm [S]
+
+
+
+
Bancroft
+ 9.50pm - 10.40pm [S]
+
+
+
+
Intruder
+ 10.40pm - 11.30pm [S]
+
+
+
+
Redemption
+ 11.30pm - 12.20am
+
+
+
+
+
+
+
+
+
diff --git a/sites/virginmediatelevision.ie/virginmediatelevision.ie.channels.xml b/sites/virginmediatelevision.ie/virginmediatelevision.ie.channels.xml
new file mode 100644
index 00000000..990dc604
--- /dev/null
+++ b/sites/virginmediatelevision.ie/virginmediatelevision.ie.channels.xml
@@ -0,0 +1,10 @@
+
+
+
+ One
+ Two
+ Three
+ Four
+ More
+
+
diff --git a/sites/virginmediatelevision.ie/virginmediatelevision.ie.config.js b/sites/virginmediatelevision.ie/virginmediatelevision.ie.config.js
new file mode 100644
index 00000000..b1fa6144
--- /dev/null
+++ b/sites/virginmediatelevision.ie/virginmediatelevision.ie.config.js
@@ -0,0 +1,82 @@
+const cheerio = require('cheerio')
+const dayjs = require('dayjs')
+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: 'virginmediatelevision.ie',
+ days: 2,
+ url({ date }) {
+ return `https://www.virginmediatelevision.ie/includes/ajax/tv_guide.php?date=${date.format(
+ 'YYYY-MM-DD'
+ )}`
+ },
+ request: {
+ cache: {
+ ttl: 60 * 60 * 1000 // 1h
+ }
+ },
+ parser({ content, channel, date }) {
+ const programs = []
+ const items = parseItems(content, channel)
+ items.forEach(item => {
+ const $item = cheerio.load(item)
+ let start = parseStart($item, date)
+ let duration = parseDuration($item)
+ let stop = start.add(duration, 'm')
+ programs.push({
+ title: parseTitle($item),
+ description: parseDescription($item),
+ sub_title: parseSubTitle($item),
+ icon: parseIcon($item),
+ start,
+ stop
+ })
+ })
+
+ return programs
+ }
+}
+
+function parseTitle($item) {
+ return $item('.info > h2').text().trim()
+}
+
+function parseDescription($item) {
+ return $item('.info').data('description')
+}
+
+function parseSubTitle($item) {
+ return $item('.info').data('subtitle')
+}
+
+function parseIcon($item) {
+ return $item('.info').data('image')
+}
+
+function parseStart($item, date) {
+ const [time] = $item('.info')
+ .data('time')
+ .match(/^\d{1,2}\.\d{2}(am|pm)/) || [null]
+
+ if (!time) return null
+
+ return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD h.mma', 'Europe/London')
+}
+
+function parseDuration($item) {
+ const duration = $item('.info > .time').data('minutes')
+
+ return duration ? parseInt(duration) : 30
+}
+
+function parseItems(content, channel) {
+ const $ = cheerio.load(content)
+
+ return $(`.programs_parent > .programs[data-channel='${channel.site_id}'] > .program`).toArray()
+}
diff --git a/sites/virginmediatelevision.ie/virginmediatelevision.ie.test.js b/sites/virginmediatelevision.ie/virginmediatelevision.ie.test.js
new file mode 100644
index 00000000..e104bad8
--- /dev/null
+++ b/sites/virginmediatelevision.ie/virginmediatelevision.ie.test.js
@@ -0,0 +1,52 @@
+// npx epg-grabber --config=sites/virginmediatelevision.ie/virginmediatelevision.ie.config.js --channels=sites/virginmediatelevision.ie/virginmediatelevision.ie.channels.xml --output=guide.xml --days=2
+
+const { parser, url } = require('./virginmediatelevision.ie.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('2023-01-31', 'YYYY-MM-DD').startOf('d')
+const channel = {
+ site_id: 'one',
+ xmltv_id: 'VirginMediaOne.ie'
+}
+
+it('can generate valid url', () => {
+ expect(url({ date })).toBe(
+ 'https://www.virginmediatelevision.ie/includes/ajax/tv_guide.php?date=2023-01-31'
+ )
+})
+
+it('can parse response', () => {
+ const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
+ let results = parser({ content, channel, date })
+ results = results.map(p => {
+ p.start = p.start.toJSON()
+ p.stop = p.stop.toJSON()
+ return p
+ })
+
+ expect(results.length).toBe(23)
+ expect(results[0]).toMatchObject({
+ start: '2023-01-31T00:00:00.000Z',
+ stop: '2023-01-31T01:00:00.000Z',
+ title: `Chasing Shadows`,
+ sub_title: '',
+ description: `A detective sergeant and expert in the field of serial killers working for the Missing Persons Bureau tries to protect the general public from evil.`,
+ icon: 'https://bcboltvirgin.akamaized.net/player/shows/1498_517x291_1528141264.jpg'
+ })
+})
+
+it('can handle empty guide', () => {
+ const results = parser({
+ date,
+ channel,
+ content: `