diff --git a/.github/workflows/berrymedia.co.kr.yml b/.github/workflows/berrymedia.co.kr.yml
new file mode 100644
index 00000000..c606c01e
--- /dev/null
+++ b/.github/workflows/berrymedia.co.kr.yml
@@ -0,0 +1,17 @@
+name: berrymedia.co.kr
+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/berrymedia.co.kr/__data__/content.html b/sites/berrymedia.co.kr/__data__/content.html
new file mode 100644
index 00000000..0f3eb707
--- /dev/null
+++ b/sites/berrymedia.co.kr/__data__/content.html
@@ -0,0 +1,386 @@
+
+ - <
+ -
+ 01.23
+
월
+
+ -
+ 01.24
+
화
+
+ -
+ 01.25
+
수
+
+ -
+ 01.26
+
목
+
+ -
+ 01.27
+
금
+
+ -
+ 01.28
+
토
+
+ -
+ 01.29
+
일
+
+ - >
+
+
+
+ -
+ 시간
+ 장르
+ 프로그램
+ 회차
+ 세부정보
+ 방송시간
+
+
+ -
+ 00:00
+
연예/오락
+
+
+ 더트롯쇼
+ 36회
+
+
+ 15
+ HD
+ 본
+ 생
+ Original
+
+ 60분
+
+ -
+ 01:00
+
교양
+
+
+ 특종세상
+ 21회
+
+
+ 15
+ HD
+ 본
+ 생
+ Original
+
+ 70분
+
+ -
+ 02:10
+
교양
+
+
+ 특종세상
+ 22회
+
+
+ 15
+ HD
+ 본
+ 생
+ Original
+
+ 70분
+
+ -
+ 03:20
+
교양
+
+
+ 나는 자연인이다
+ 124회
+
+
+ ALL
+ HD
+ 재
+ 생
+ Original
+
+ 70분
+
+ -
+ 04:30
+
패션/뷰티
+
+
+ 탑시크릿란제리HD
+ 1회
+
+
+ 19
+ HD
+ 본
+ 생
+ Original
+
+ 30분
+
+ -
+ 05:00
+
연예/오락
+
+
+ SALON INTERNATIONAL DE LA LINGERIE
+ 7회
+
+
+ 19
+ HD
+ 본
+ 생
+ Original
+
+ 30분
+
+ -
+ 05:30
+
연예/오락
+
+
+ 노컷 란제리 페스티벌
+ 14회
+
+
+ 19
+ HD
+ 본
+ 생
+ Original
+
+ 60분
+
+ -
+ 06:30
+
연예/오락
+
+
+ 요가에 빠지다
+ 7회
+
+
+ 15
+ HD
+ 본
+ 생
+ Original
+
+ 40분
+
+ -
+ 07:10
+
연예/오락
+
+
+ 이제 만나러 갑니다
+ 20회
+
+
+ 12
+ HD
+ 본
+ 생
+ Original
+
+ 70분
+
+ -
+ 08:20
+
연예/오락
+
+
+ 미스터트롯 영웅들의 탄생
+ 5회
+
+
+ 15
+ HD
+ 재
+ 생
+ Original
+
+ 100분
+
+ -
+ 10:00
+
연예/오락
+
+
+ 미스터트롯 영웅들의 탄생
+ 6회
+
+
+ 15
+ HD
+ 재
+ 생
+ Original
+
+ 100분
+
+ -
+ 11:40
+
연예/오락
+
+
+ 불타는 트롯맨 5
+ 3회
+
+
+ 15
+ HD
+ 재
+ 생
+ Original
+
+ 80분
+
+ -
+ 13:00
+
연예/오락
+
+
+ 화요일은 밤이 좋아
+ 17회
+
+
+ 15
+ HD
+ 본
+ 생
+ Original
+
+ 130분
+
+ -
+ 15:10
+
연예/오락
+
+
+ 불타는 트롯맨 6
+ 3회
+
+
+ 15
+ HD
+ 재
+ 생
+ Original
+
+ 110분
+
+ -
+ 17:00
+
교양
+
+
+ 행복한 힐링 메이트
+ 41회
+
+
+ 15
+ HD
+ 재
+ 생
+ Original
+
+ 60분
+
+ -
+ 18:00
+
연예/오락
+
+
+ 내일은 미스트롯 2
+ 7회
+
+
+ 15
+ HD
+ 본
+ 생
+ Original
+
+ 180분
+
+ -
+ 21:00
+
연예/오락
+
+
+ 불타는 트롯맨 3
+ 2회
+
+
+ 15
+ HD
+ 재
+ 생
+ Original
+
+ 110분
+
+ -
+ 22:50
+
교양
+
+
+ 나는 자연인이다
+ 154회
+
+
+ ALL
+ HD
+ 본
+ 생
+ Original
+
+ 70분
+
+
\ No newline at end of file
diff --git a/sites/berrymedia.co.kr/__data__/no_content.html b/sites/berrymedia.co.kr/__data__/no_content.html
new file mode 100644
index 00000000..e1755d0a
--- /dev/null
+++ b/sites/berrymedia.co.kr/__data__/no_content.html
@@ -0,0 +1,44 @@
+
+ - <
+ -
+ 01.23
+
월
+
+ -
+ 01.24
+
화
+
+ -
+ 01.25
+
수
+
+ -
+ 01.26
+
목
+
+ -
+ 01.27
+
금
+
+ -
+ 01.28
+
토
+
+ -
+ 01.29
+
일
+
+ - >
+
+
+
+ -
+ 시간
+ 장르
+ 프로그램
+ 회차
+ 세부정보
+ 방송시간
+
+
+
\ No newline at end of file
diff --git a/sites/berrymedia.co.kr/berrymedia.co.kr.channels.xml b/sites/berrymedia.co.kr/berrymedia.co.kr.channels.xml
new file mode 100644
index 00000000..8d09501d
--- /dev/null
+++ b/sites/berrymedia.co.kr/berrymedia.co.kr.channels.xml
@@ -0,0 +1,11 @@
+
+
+
+ BallTV
+ CookTV
+ GMTV
+ GTV
+ 생활체육TV
+ UHDDreamTV
+
+
diff --git a/sites/berrymedia.co.kr/berrymedia.co.kr.config.js b/sites/berrymedia.co.kr/berrymedia.co.kr.config.js
new file mode 100644
index 00000000..f8b8f2a4
--- /dev/null
+++ b/sites/berrymedia.co.kr/berrymedia.co.kr.config.js
@@ -0,0 +1,93 @@
+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)
+
+dayjs.Ls.en.weekStart = 1
+
+module.exports = {
+ site: 'berrymedia.co.kr',
+ days: 2,
+ url({ channel }) {
+ return `http://www.berrymedia.co.kr/schedule_proc${channel.site_id}.php`
+ },
+ request: {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
+ 'X-Requested-With': 'XMLHttpRequest'
+ },
+ data({ date }) {
+ let params = new URLSearchParams()
+ let startOfWeek = date.startOf('week').format('YYYY-MM-DD')
+ let endOfWeek = date.endOf('week').format('YYYY-MM-DD')
+
+ params.append('week', `${startOfWeek}~${endOfWeek}`)
+ params.append('day', date.format('YYYY-MM-DD'))
+
+ return params
+ }
+ },
+ parser({ content, date }) {
+ const programs = []
+ const items = parseItems(content)
+ items.forEach(item => {
+ const $item = cheerio.load(item)
+ const prev = programs[programs.length - 1]
+ 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),
+ category: parseCategory($item),
+ rating: parseRating($item),
+ start,
+ stop
+ })
+ })
+
+ return programs
+ }
+}
+
+function parseStart($item, date) {
+ const time = $item('span:nth-child(1)').text().trim()
+
+ return dayjs.tz(`${date.format('YYYY-MM-DD')} ${time}`, 'YYYY-MM-DD HH:mm', 'Asia/Seoul')
+}
+
+function parseTitle($item) {
+ return $item('span.sdfsdf').clone().children().remove().end().text().trim()
+}
+
+function parseCategory($item) {
+ return $item('span:nth-child(2) > p').text().trim()
+}
+
+function parseRating($item) {
+ const rating = $item('span:nth-child(5) > p:nth-child(1)').text().trim()
+
+ return rating
+ ? {
+ system: 'KMRB',
+ value: rating
+ }
+ : null
+}
+
+function parseItems(content, channel, date) {
+ const $ = cheerio.load(content)
+
+ return $('.sc_time dd').toArray()
+}
diff --git a/sites/berrymedia.co.kr/berrymedia.co.kr.test.js b/sites/berrymedia.co.kr/berrymedia.co.kr.test.js
new file mode 100644
index 00000000..7396aafc
--- /dev/null
+++ b/sites/berrymedia.co.kr/berrymedia.co.kr.test.js
@@ -0,0 +1,79 @@
+// npx epg-grabber --config=sites/berrymedia.co.kr/berrymedia.co.kr.config.js --channels=sites/berrymedia.co.kr/berrymedia.co.kr.channels.xml --output=guide.xml --days=2
+
+const { parser, url, request } = require('./berrymedia.co.kr.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-26', 'YYYY-MM-DD').startOf('d')
+const channel = {
+ site_id: '',
+ xmltv_id: 'GTV.kr'
+}
+
+it('can generate valid url', () => {
+ expect(url({ channel })).toBe('http://www.berrymedia.co.kr/schedule_proc.php')
+})
+
+it('can generate request method', () => {
+ expect(request.method).toBe('POST')
+})
+
+it('can generate valid request headers', () => {
+ expect(request.headers).toMatchObject({
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
+ 'X-Requested-With': 'XMLHttpRequest'
+ })
+})
+
+it('can generate valid request data', () => {
+ let params = request.data({ date })
+
+ expect(params.get('week')).toBe('2023-01-23~2023-01-29')
+ expect(params.get('day')).toBe('2023-01-26')
+})
+
+it('can parse response', () => {
+ const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.html'), 'utf8')
+ let results = parser({ content, date })
+ results = results.map(p => {
+ p.start = p.start.toJSON()
+ p.stop = p.stop.toJSON()
+ return p
+ })
+
+ expect(results[0]).toMatchObject({
+ start: '2023-01-25T15:00:00.000Z',
+ stop: '2023-01-25T16:00:00.000Z',
+ title: `더트롯쇼`,
+ category: '연예/오락',
+ rating: {
+ system: 'KMRB',
+ value: '15'
+ }
+ })
+
+ expect(results[17]).toMatchObject({
+ start: '2023-01-26T13:50:00.000Z',
+ stop: '2023-01-26T14:20:00.000Z',
+ title: `나는 자연인이다`,
+ category: '교양',
+ rating: {
+ system: 'KMRB',
+ value: 'ALL'
+ }
+ })
+})
+
+it('can handle empty guide', () => {
+ const results = parser({
+ date,
+ content: fs.readFileSync(path.resolve(__dirname, '__data__/no_content.html'), 'utf8')
+ })
+
+ expect(results).toMatchObject([])
+})