diff --git a/scripts/commands/sites/update.ts b/scripts/commands/sites/update.ts
new file mode 100644
index 00000000..d1f7e8a4
--- /dev/null
+++ b/scripts/commands/sites/update.ts
@@ -0,0 +1,58 @@
+import { Logger, Storage, Collection, Dictionary } from '@freearhey/core'
+import { IssueLoader, HTMLTable, Markdown } from '../../core'
+import { Issue, Site } from '../../models'
+import { SITES_DIR, DOT_SITES_DIR } from '../../constants'
+import path from 'path'
+
+async function main() {
+ const logger = new Logger({ disabled: true })
+ const loader = new IssueLoader()
+ const storage = new Storage(SITES_DIR)
+ const sites = new Collection()
+
+ logger.info('loading list of sites')
+ const folders = await storage.list('*/')
+
+ logger.info('loading issues...')
+ const issues = await loadIssues(loader)
+
+ logger.info('putting the data together...')
+ folders.forEach((domain: string) => {
+ const filteredIssues = issues.filter((issue: Issue) => domain === issue.data.get('site'))
+ const site = new Site({
+ domain,
+ issues: filteredIssues
+ })
+
+ sites.add(site)
+ })
+
+ logger.info('creating sites table...')
+ let data = new Collection()
+ sites.forEach((site: Site) => {
+ data.add([
+ `${site.domain}`,
+ site.getStatus().emoji,
+ site.getIssues().all().join(', ')
+ ])
+ })
+
+ const table = new HTMLTable(data.all(), [{ name: 'Site' }, { name: 'Status' }, { name: 'Notes' }])
+
+ const readmeStorage = new Storage(DOT_SITES_DIR)
+ await readmeStorage.save('_table.md', table.toString())
+
+ logger.info('updating sites.md...')
+ const configPath = path.join(DOT_SITES_DIR, 'config.json')
+ const sitesMarkdown = new Markdown(configPath)
+ sitesMarkdown.compile()
+}
+
+main()
+
+async function loadIssues(loader: IssueLoader) {
+ const issuesWithStatusWarning = await loader.load({ labels: ['broken guide', 'status:warning'] })
+ const issuesWithStatusDown = await loader.load({ labels: ['broken guide', 'status:down'] })
+
+ return issuesWithStatusWarning.concat(issuesWithStatusDown)
+}
diff --git a/scripts/constants.ts b/scripts/constants.ts
index 7498bf81..e15b8b45 100644
--- a/scripts/constants.ts
+++ b/scripts/constants.ts
@@ -2,3 +2,7 @@ export const SITES_DIR = process.env.SITES_DIR || './sites'
export const GUIDES_DIR = process.env.GUIDES_DIR || './guides'
export const DATA_DIR = process.env.DATA_DIR || './temp/data'
export const API_DIR = process.env.API_DIR || '.api'
+export const DOT_SITES_DIR = process.env.DOT_SITES_DIR || './.sites'
+export const TESTING = process.env.NODE_ENV === 'test' ? true : false
+export const OWNER = 'iptv-org'
+export const REPO = 'epg'
diff --git a/scripts/core/htmlTable.ts b/scripts/core/htmlTable.ts
new file mode 100644
index 00000000..1caa85fa
--- /dev/null
+++ b/scripts/core/htmlTable.ts
@@ -0,0 +1,46 @@
+type Column = {
+ name: string
+ nowrap?: boolean
+ align?: string
+}
+
+type DataItem = string[]
+
+export class HTMLTable {
+ data: DataItem[]
+ columns: Column[]
+
+ constructor(data: DataItem[], columns: Column[]) {
+ this.data = data
+ this.columns = columns
+ }
+
+ toString() {
+ let output = '
\n'
+
+ output += ' \n '
+ for (const column of this.columns) {
+ output += `${column.name} | `
+ }
+ output += '
\n \n'
+
+ output += ' \n'
+ for (const item of this.data) {
+ output += ' '
+ let i = 0
+ for (const prop in item) {
+ const column = this.columns[i]
+ const nowrap = column.nowrap ? ' nowrap' : ''
+ const align = column.align ? ` align="${column.align}"` : ''
+ output += `${item[prop]} | `
+ i++
+ }
+ output += '
\n'
+ }
+ output += ' \n'
+
+ output += '
'
+
+ return output
+ }
+}
diff --git a/scripts/core/index.ts b/scripts/core/index.ts
index fca249a5..14a0ffa0 100644
--- a/scripts/core/index.ts
+++ b/scripts/core/index.ts
@@ -10,3 +10,7 @@ export * from './guide'
export * from './apiChannel'
export * from './apiClient'
export * from './queueCreator'
+export * from './issueLoader'
+export * from './issueParser'
+export * from './htmlTable'
+export * from './markdown'
diff --git a/scripts/core/issueLoader.ts b/scripts/core/issueLoader.ts
new file mode 100644
index 00000000..28697e58
--- /dev/null
+++ b/scripts/core/issueLoader.ts
@@ -0,0 +1,40 @@
+import { Collection } from '@freearhey/core'
+import { restEndpointMethods } from '@octokit/plugin-rest-endpoint-methods'
+import { paginateRest } from '@octokit/plugin-paginate-rest'
+import { Octokit } from '@octokit/core'
+import { IssueParser } from './'
+import { TESTING, OWNER, REPO } from '../constants'
+
+const CustomOctokit = Octokit.plugin(paginateRest, restEndpointMethods)
+const octokit = new CustomOctokit()
+
+export class IssueLoader {
+ async load({ labels }: { labels: string[] | string }) {
+ labels = Array.isArray(labels) ? labels.join(',') : labels
+ let issues: object[] = []
+ if (TESTING) {
+ switch (labels) {
+ case 'broken guide,status:warning':
+ issues = require('../../tests/__data__/input/issues/broken_guide_warning.js')
+ break
+ case 'broken guide,status:down':
+ issues = require('../../tests/__data__/input/issues/broken_guide_down.js')
+ break
+ }
+ } else {
+ issues = await octokit.paginate(octokit.rest.issues.listForRepo, {
+ owner: OWNER,
+ repo: REPO,
+ per_page: 100,
+ labels,
+ headers: {
+ 'X-GitHub-Api-Version': '2022-11-28'
+ }
+ })
+ }
+
+ const parser = new IssueParser()
+
+ return new Collection(issues).map(parser.parse)
+ }
+}
diff --git a/scripts/core/issueParser.ts b/scripts/core/issueParser.ts
new file mode 100644
index 00000000..2fe2ddd8
--- /dev/null
+++ b/scripts/core/issueParser.ts
@@ -0,0 +1,34 @@
+import { Dictionary } from '@freearhey/core'
+import { Issue } from '../models'
+
+const FIELDS = new Dictionary({
+ Site: 'site'
+})
+
+export class IssueParser {
+ parse(issue: { number: number; body: string; labels: { name: string }[] }): Issue {
+ const fields = issue.body.split('###')
+
+ const data = new Dictionary()
+ fields.forEach((field: string) => {
+ let parsed = field.split(/\r?\n/).filter(Boolean)
+ let _label = parsed.shift()
+ _label = _label ? _label.trim() : ''
+ let _value = parsed.join('\r\n')
+ _value = _value ? _value.trim() : ''
+
+ if (!_label || !_value) return data
+
+ const id: string = FIELDS.get(_label)
+ const value: string = _value === '_No response_' || _value === 'None' ? '' : _value
+
+ if (!id) return
+
+ data.set(id, value)
+ })
+
+ const labels = issue.labels.map(label => label.name)
+
+ return new Issue({ number: issue.number, labels, data })
+ }
+}
diff --git a/scripts/core/markdown.ts b/scripts/core/markdown.ts
new file mode 100644
index 00000000..b43b5608
--- /dev/null
+++ b/scripts/core/markdown.ts
@@ -0,0 +1,13 @@
+import markdownInclude from 'markdown-include'
+
+export class Markdown {
+ filepath: string
+
+ constructor(filepath: string) {
+ this.filepath = filepath
+ }
+
+ compile() {
+ markdownInclude.compileFiles(this.filepath)
+ }
+}
diff --git a/scripts/models/index.ts b/scripts/models/index.ts
new file mode 100644
index 00000000..1065611d
--- /dev/null
+++ b/scripts/models/index.ts
@@ -0,0 +1,2 @@
+export * from './issue'
+export * from './site'
diff --git a/scripts/models/issue.ts b/scripts/models/issue.ts
new file mode 100644
index 00000000..7eb241e5
--- /dev/null
+++ b/scripts/models/issue.ts
@@ -0,0 +1,24 @@
+import { Dictionary } from '@freearhey/core'
+import { OWNER, REPO } from '../constants'
+
+type IssueProps = {
+ number: number
+ labels: string[]
+ data: Dictionary
+}
+
+export class Issue {
+ number: number
+ labels: string[]
+ data: Dictionary
+
+ constructor({ number, labels, data }: IssueProps) {
+ this.number = number
+ this.labels = labels
+ this.data = data
+ }
+
+ getURL() {
+ return `https://github.com/${OWNER}/${REPO}/issues/${this.number}`
+ }
+}
diff --git a/scripts/models/site.ts b/scripts/models/site.ts
new file mode 100644
index 00000000..1e7dcadd
--- /dev/null
+++ b/scripts/models/site.ts
@@ -0,0 +1,57 @@
+import { Collection } from '@freearhey/core'
+import { Issue } from './'
+
+enum StatusCode {
+ DOWN = 'down',
+ WARNING = 'warning',
+ OK = 'ok'
+}
+
+type Status = {
+ code: StatusCode
+ emoji: string
+}
+
+type SiteProps = {
+ domain: string
+ issues: Collection
+}
+
+export class Site {
+ domain: string
+ issues: Collection
+
+ constructor({ domain, issues }: SiteProps) {
+ this.domain = domain
+ this.issues = issues
+ }
+
+ getStatus(): Status {
+ const issuesWithStatusDown = this.issues.filter((issue: Issue) =>
+ issue.labels.find(label => label === 'status:down')
+ )
+ if (issuesWithStatusDown.notEmpty())
+ return {
+ code: StatusCode.DOWN,
+ emoji: '🔴'
+ }
+
+ const issuesWithStatusWarning = this.issues.filter((issue: Issue) =>
+ issue.labels.find(label => label === 'status:warning')
+ )
+ if (issuesWithStatusWarning.notEmpty())
+ return {
+ code: StatusCode.WARNING,
+ emoji: '🟡'
+ }
+
+ return {
+ code: StatusCode.OK,
+ emoji: '🟢'
+ }
+ }
+
+ getIssues(): Collection {
+ return this.issues.map((issue: Issue) => issue.getURL())
+ }
+}